<?php

include_once 'class.nce_email.php';
include_once 'global_variable_replacer.php';
error_reporting(E_ALL ^ E_NOTICE);//TMP

if (class_exists('Nce_form'))
{
	return;
}

/**
 * @todo - write a function that creates an NCfield object from tag content
 * @todo - maybe write a function that will create a FormTag object, this should have a parse function which takes an EE tag string and pulls out its parameters, values and args
 */
class Nce_form {

	var $version = "3.0";
	var $config;
	var $phrasebook;
	var $form_name;
	var $csv_delimiter = ",";
	
	function Nce_form()
	{
		$this->init();
	}

	function init()
	{
		$this->replace_global_variables();
		$this->load_form_config();
		$this->load_phrasebooks($this->config['language']);
	}
	
	function replace_global_variables()
	{
		global $TMPL;
		
		$tagdata = $TMPL->tagdata;
		$replacer = new Global_variable_replacer();

		foreach($replacer->globals_lookup as $key => $value)
		{
			preg_match_all("/{$key}/", $tagdata, $matches);
			//print_r($matches);//TMP

			if(sizeof($matches[0]) > 0)
			{
				$tagdata = str_replace("{".$key."}", $value, $tagdata);
			}
		}
		
		$TMPL->tagdata = $tagdata;
		
		$this->rebuild_var_arrays();
	}
	
	// rebuilds the EE variable arrays to handle global variables within tags
	function rebuild_var_arrays()
	{
		global $TMPL, $FNS;
		$tagdata = $TMPL->tagdata;
		
		//update config tags
		$regex = LD."config".RD."(.*?)".LD.SLASH."config".RD;
		preg_match_all("/".$regex."/s", $tagdata, $matches);
		$config_tags = $matches[1];
		//print_r($config_tags);//TMP
		$config_vars = $FNS->assign_variables($config_tags[0], '/');
		
		//replace single vars
		foreach($config_vars['var_single'] as $key => $val)
		{
			$TMPL->var_single[$key] = $val;
		}
		
		//update element tags
		$regex = LD."element(.*?)".RD."(.*?)".LD.SLASH."element".RD;
		preg_match_all("/".$regex."/s", $tagdata, $matches);
		$element_vars = array();
		foreach($matches[0] as $key => $val)
		{
			$val = str_replace('&#47;', '/', $val);
			$element_vars[] = $FNS->assign_variables($val, '/');
		}
		
		//update pair vars
		//print_r($TMPL->var_pair);//TMP
		foreach($element_vars as $var)
		{
			foreach($var['var_pair'] as $key => $val)
			{
				$key = str_replace('/', '&#47;', $key);
				$val = str_replace('/', '&#47;', $val);
				$TMPL->var_pair[$key] = $val;
			}
		}
		//print_r($TMPL->var_pair);//TMP
	}
	
	//  parse the {config} block into $this->config
	function load_form_config()
	{
		global $TMPL;
		
		$tagdata = $TMPL->tagdata;
		
		$config_tags = TagObject::create_tag_objects($tagdata, 'config');
		$config_tag = $config_tags[0];
		//print_r($config_tag);//TMP
		if ($config_tag != null)
		{
			$TMPL->tagdata = str_replace($config_tag->html, "", $TMPL->tagdata); // clear config tags from template		
			$settings_tags = $config_tag->create_inner_tag_objects('setting');
			//print_r($settings_tags);//TMP
			
			foreach ($settings_tags as $tag)
			{
				$key = $tag->params['name'];
				$value = $tag->params['value'];
				//print_r($value);//TMP
				$this->config[$key] = $value;
			}
		}

		//print("<pre>config:\n");//TMP
		//print_r($this->config);//TMP
		//print("</pre>");//TMP
		//print($tagdata);//TMP
	}

	/**
	 * load the default and custom phrasebooks and merge the two (custom phrases overwrite default ones if same key exists)
	 */
	function load_phrasebooks($lang="en", $xml="")
	{
		global $DB;
		
		if($xml == "")
		{
			$results = $DB->query("SELECT template_data FROM exp_templates WHERE template_name = 'nce_form_phrasebook.xml'");
			if ($results->num_rows > 0)
			{
				$default_phrasebook_file = $results->row['template_data'];
			}			
	
			$this->phrasebook = self::phrasebook_to_array($default_phrasebook_file, $lang);

			if (!empty($this->config['custom_phrasebook_template']))	//pass the template and data through.
			{
				$custom_phrasebook_xml = $this->get_custom_template_content($this->config['custom_phrasebook_template']);

				if (!empty($custom_phrasebook_xml))
				{
					$custom_phrasebook = self::phrasebook_to_array($custom_phrasebook_xml, $lang);
				}
			}
				
			if (!empty($custom_phrasebook)) // overwrite phrases
			{
				foreach ($custom_phrasebook as $key => $value)
				{
					$this->phrasebook[$key] = $value;
				}
			}
		}
		else
		{
			$this->phrasebook = self::phrasebook_to_array($xml, $lang);
		}
	}
	
	function sanitise_submission()
	{
		foreach ($_REQUEST as $key => $value)
		{
			$sanitised = str_replace(array("<",">","{", "}", "\\"), " ", $value);
			$sanitised = str_replace('"', "'", $sanitised);
			$sanitised = str_replace(array("\r\n","\n","\r"), " ", $sanitised);
			$_REQUEST[$key] = $sanitised;
		}
	}
	
	/**
	 *
	 */
	function ncform()
	{
		$self = $_SERVER['PHP_SELF'];
	
		//@todo - out the form, handle submission
		global $TMPL;
		
		$html = $TMPL->tagdata;
		$this->form_name = $TMPL->fetch_param('name');
		$validation_point = $TMPL->fetch_param('validation_point');
		
		$this->sanitise_submission();
		
		$html = $this->get_replaced_phrases($html); // do the localisation
		
		$field_tags = TagObject::create_tag_objects($html, 'field'); // get all the field tags
		
		$field_objects = array();
		
		$do_submit = false;
		foreach ($field_tags as $tag) // create field objects for each tag
		{
//			print("<hr>field inner html:\n".$tag->inner_html."\n<hr/>\n");//TMP
			$field = new FieldObject($tag);
			$field_objects[$field->name] = $field;
			if ($field->type == 'submit')
			{
				$do_submit |= array_key_exists($field->name, $_REQUEST); // has the form been submitted?
			}
			else if ($field->type == 'image')
			{
				$do_submit |= array_key_exists($field->name."_x", $_REQUEST); // has the form been submitted?
			}
		}
		
		$is_submission_valid = true;
		foreach ($field_objects as $field)
		{
			if ($do_submit) // form was submitted, do validation
			{
				$is_submission_valid &= $field->validate();		
//				print("is submission valid? ($is_submission_valid) field: $field->name<br/>\n");//TMP
			}
			else // no submission, clear out error messages
			{
				if (empty($validation_point) || $validation_point == 'backend')
				{					
					$field->clear_error_tags();
				}
				else
				{
					//@todo - output javascript for frontend validation (not decided how this will work yet)
				}
			}
		}

		$form_tags = TagObject::create_tag_objects($html, 'form');
		if (sizeof($form_tags) == 0)
		{
			$html = "Missing {form} tags<br/>\n";
			return $html;
		}
		else
		{			
			$form_tag = $form_tags[0];
		}

		if ($do_submit && $is_submission_valid) // perform form action - send email, save to CSV/database, pass to another plugin/module etc.
		{
			$html = $this->handle_valid_submission($html, $form_tag, $field_objects);
		}
		else // output the form
		{									
			if (!empty($form_tag))
			{
				if (empty($form_tag->params['method']))
				{
					$form_tag->params['method'] = 'post';
				}
				
				$form_action = $form_tag->params['action'];
				if (empty($form_action))
				{
					$form_action = $self;
				}
				
				$form_action .= (strpos($form_action, "?") !== false ? '&' : '?');
				$form_action .= 'ncf_sbmt=1';
				$form_tag->params['action'] = $form_action;
				
				$attributes = $form_tag->single_params_to_string();
				
				$form_html = "\n<form name=\"$this->form_name\" $attributes >\n"; // open form
				$form_html .= $form_tag->inner_html;
				$form_html .= "\n</form>\n";
				
				$html = $form_tag->replace_with($html, $form_html);

				foreach ($field_objects as $field)
				{
//				print("$field->name contents: ".$field->field_tag->inner_html."<br/>\n");//TMP
					$html = $field->output_self($html);
				}
			}
			
			// clear result tag		
			$tags = TagObject::create_tag_objects($html, 'result');		
			$result_tag = $tags[0];		
			$html = $result_tag->replace_with_regex($html, "");
		}
		
		return $html;
	}
	
	/**
	 *  Send email, save to CSV/database, pass to another plugin/module etc.
	 */
	function handle_valid_submission($html, $form_tag, $field_objects)
	{
		global $TMPL, $DB;
		
//		print("handle_valid_submission<br/>\n");//TMP
		$html = $form_tag->replace_with_regex($html, ""); // clear the form - @todo will need to output the {result} tags
//		print("html: <hr>".htmlspecialchars($html)."<hr/>");//TMP
		
		$result_html = "";

		$submit_action = $TMPL->fetch_param('submit_action');
		
		$actions = explode(",", $submit_action);
		
		$errors = array();

		$field_replacements = array();
		foreach($field_objects as $field)
		{
			$field_name = $field->name;
			$field_value = $_REQUEST[$field_name];			
			$field_replacements[$field_name] = $field_value; // the values to replace the tags in the template(s).
		}
		
		foreach ($actions as $action)
		{		
			$parts = explode(":", $action);
			$action = $parts[0];
			$arg = $parts[1];
			
//			$result_html .= "[debug] performing the form action '$action'<br/>\n";//TMP
			switch ($action)
			{
				case 'email_to_user':
					
					//Need to sort out the field objects.
					$email_addresses = array();
					$tags = array();
					$values = array();
					foreach($field_objects as $field)
					{
						if(preg_match("/\be-?mail\b/i", $field_name))
						{
//							$result_html .= "[debug] Sending to '$field_value'<br/>\n";//TMP - debug
							$email_addresses[] = $field_value;	// add this email field to the list of emails to be sent to. 
						}
					} 
					
//				print_r($field_replacements);//TMP
					
					//drop through to next case.
				case 'email_to':
					$to_address = (empty($to_address) ? $arg : $to_address);
					$emails_addresses[] = $to_address;
					//$first_name = ""; //@todo - get the recipient name from the config
					
					//@todo - get the mail template if exists
					
					$this->send_email($email_addresses, $field_replacements);					
					break;
					
				case 'csv' :
					$csv_path = html_entity_decode($arg);
					$success = $this->write_submission_to_csv($csv_path, $field_objects); // write to the CSV
					if (!$success)
					{
						$errors[] = "Could not write to CSV file $csv_path (cwd: ".realpath(".").")";//TMP
					}
					break;
					
				case 'database' :
					$db_path = $arg;
					// $success = $this->write_submission_to_database($db_path, $field_objects); //@todo: write to the database
					if (!$success)
					{
						$errors[] = "Could not write to database";//TMP
					}
					break;
				case 'submit_to_plugin' :
				case 'submit_to_module' :
					$parts = explode(".", $arg);
					$class_name = $parts[0];
					$function_name = $parts[1];
					$script_dir = dirname(dirname(__FILE__));
					$name = strtolower($class_name);
					
					if ($action == 'submit_to_plugin')
					{
						$include_path = $script_dir . "/plugins/pi.$name.php";
					}
					else if ($action == 'submit_to_module')
					{
						$include_path = $script_dir . "/modules/$name/mod.$name.php";
					}
					
					// include the class and run the handler function
//					print("include path: $include_path<br/>\n");//TMP
					include_once $include_path;
//					print("$action: $class_name"."->"."$function_name<br/>\n");//TMP
					$handler = new $class_name();
					$result_html .= $handler->$function_name($this->config);
					
					//@todo - error handling
					break;
					
				default :
					$errors[] = "Sorry, the action '$action' you specified was not recognised.";
			}
		}
				
		$tags = TagObject::create_tag_objects($html, 'result');		
		$result_tag = $tags[0];		

		if (sizeof($errors) > 0) // output system errors
		{
			$result_html .= "There were errors: <ul>\n";
			foreach ($errors as $error)
			{
				$result_html .= "<li>$error</li>\n";
			}			
			$result_html .= "</ul>\n";
		}
		else 
		{
			$custom_template_data = $this->get_custom_template_content($this->config['custom_result_template'], $field_replacements);
			
			if (empty($custom_template_data))
			{
				$tag_content = $result_tag->inner_html;
				$result_html .= $tag_content;
			}
			else // display custom result template if exists
			{
				$result_html .= $custom_template_data;
			}
		}
		
		// output result template		
		$html = $result_tag->replace_with_regex($html, $result_html);
			
		return $html;
	}
	
	/**
	 * Load in the custom template content from the specified location given in the config e.g. ncform/custom_thankyou_template.html
	 */
	function get_custom_template_content($template_location, $field_replacements=array())
	{
		global $DB;
	
		$custom_template = explode("/", $template_location);
		$template_group = $custom_template[0];
		$template_name = $custom_template[1];
		
		$sql = "SELECT template_data FROM exp_templates, exp_template_groups WHERE ".
			   "template_name = '$template_name' AND group_name = '$template_group' AND ".
			   "exp_templates.group_id = exp_template_groups.group_id";

//		print("\nsql: $sql<br/>\n");//TMP	   
			   
	   $results = $DB->query($sql);
		
		if ($results->num_rows > 0)
		{
			$template_data = $results->row['template_data'];
		}							
		
		$finished = false;
		while (!$finished && preg_match("/".LD."embed=(.+)".RD."/siU", $template_data, $matches)) // pull in the embed if there is one
		{
			$val = $matches[1];
//			print("embed=$val<br/>\n");//TMP
			$parts = explode(":", $val);
			if (sizeof($parts) == 2)
			{
				$site_name = $parts[0];
				$template_location = $parts[1];
			}
			else
			{
				$template_location = $parts[0];
			}
			
			$custom_template = explode("/", $template_location);
			$template_group = $custom_template[0];
			$template_name = $custom_template[1];

			if (!empty($site_name))
			{
				$tables_extra = ", exp_sites";
				$where_extra = " AND exp_sites.site_id = exp_templates.site_id AND exp_sites.site_name = '$site_name'";
			}			
			
			$sql = "SELECT template_data FROM exp_templates, exp_template_groups $tables_extra WHERE ".
				   "template_name = '$template_name' AND group_name = '$template_group' AND ".
				   "exp_templates.group_id = exp_template_groups.group_id $where_extra";

//			print("\nsql: $sql<br/>\n");//TMP
				   
		   $results = $DB->query($sql);
			
			if ($results->num_rows > 0)
			{
				$template_data = $results->row['template_data'];
			}
			else
			{
				$finished = true;
			}		
//			print("\ntemplate_data: $template_data<br/>\n\n");//TMP
			
		}
		
		if (sizeof($field_replacements) > 0) // replace any fields in the template with the replacement values
		{
			foreach ($field_replacements as $key => $value)
			{
				$template_data = str_replace("{".$key."}", $value, $template_data);				
			}
		}
		
		$template_data = trim($template_data);
		
//		print("<hr/>".$template_data."</hr>");//TMP
			
		return $template_data;
	}
	
	/**
	 * Replace all {phrase} tags using keys from the phrasebook(s)
	 */
	function get_replaced_phrases($html)
	{
		$phrase_tags = TagObject::create_tag_objects($html, 'phrase');
		
		foreach ($phrase_tags as $tag)
		{
			$key = $tag->params['key'];
			$value = $this->phrasebook[$key];
			
			if (!empty($value))
			{
				$html = $tag->replace_with($html, $value);
			}
		}
		
		return $html;
		
	} // end get_replaced_phrases
	
	/**
	 * Write submitted fields and values to a CSV file
	 */
	function write_submission_to_csv($csv_path, $field_objects) 
	{
		$success = true;
	
//		print("writing to CSV<br/>\n");//TMP
//		print("cwd: ".realpath(".")."<br/>\n");//TMP
//		print("csv path: $csv_path<br/>\n");//TMP
			
		$csv_values = array();
		
		foreach($field_objects as $field) 
		{
			$process = trim($field->field_tag->params['process']);
			
			if ($process != 'no' && $process != '0' && $process != 'false')
			{
				$field_name = $field->name;

				$value = $field->get_submitted_value(); // an array (for multi-element fields) or sinlge value
				
				if (is_array($value)) // handle a multi-element field
				{
					if ($field->type == 'date')
					{
						$value = $value['years'] . "-" . $value['months'] . "-" . $value['days'];
					}
					else
					{
						$temp = implode("�", $value);
						$temp = trim($temp, "�");
						$temp = preg_replace("/�+/", "�", $temp);					
						$temp = str_replace(",", " ", $temp); // replace commas with spaces
						$value = str_replace("�", ",", $temp); // comma-separate multiple answers
					}
				}

//			print("field name: $field_name value: $value<br/>\n");//TMP
				
				$csv_values[$field_name] = $value;
			}
		}
		
		if (!file_exists($csv_path) || filesize($csv_path) == 0) // file doesn't exist so write out the header
		{
			$names = array_keys($csv_values);
			$csv_header = implode('","', $names);
			$csv_header = '"' . $csv_header . '"' . "\n";
			file_put_contents($csv_path, $csv_header, FILE_APPEND + LOCK_EX) or $success = false;
//				or print("Could not write CSV header to: $csv_path\n");
		}

		foreach($csv_values as $k => $v) // create the CSV line
		{
			$v = trim($v, ",");
			$v = str_replace('"', '\'', $v); // replace double-quotes with single
			$v = str_replace("\\", " ", $v); // replace back-slash with space
			$csv_line .= '"' . $v . '",'; // wrap value in double-quotes and delimit with a comma
		}
		$csv_line = trim($csv_line, ",");
		$csv_line .= "\n";
		file_put_contents($csv_path, $csv_line, FILE_APPEND + LOCK_EX) or $success = false;
//			or print("Could not write CSV data to: $csv_path\n");
	
		return $success;
	
	} // end write_submission_to_csv
					
	
	/**
	 *
	 */
	function send_email($recipients, $field_replacements)
	{
		$success = false;
		if(!empty($recipients))
		{
			//Set the sent from details.
			$sender_address = $this->config['email_sender_address'];
			$sender_name = $this->config['email_sender_name'];
			$mail_host = $this->config['email_server'];
			$mailer = new EmailSender($sender_address, $sender_name, $mail_host);
			// set a default email subject and body.
			$mailer->email_subject = $this->phrasebook["default-email-subject"];
			$mailer->body_text = $this->phrasebook["default-email-body"];
			
			if (!empty($this->config['email_template']))	//pass the template and data through.
			{
				$email_template = explode("/", $this->config['email_template']);
				$mailer->template_group = $email_template[0];
				$mailer->template_name = $email_template[1];
			}
			
			$custom_subject_key = $this->config['email_subject_key'];
			if (!empty($custom_subject_key))
			{
				$mailer->email_subject = $this->phrasebook[$custom_subject_key];
			}
			
			// Add the body tags and values.	
			$mailer->field_replacements = $field_replacements;

//			print("replacements:<br/>\n");//TMP
//			print_r($mailer->field_replacements);//TMP
			
			//Get all the email fields on the form, loop through and send seperately to each.
			foreach($recipients	as $address)
			{
				$mailer->addRecipient($address, $address);
				$success = $mailer->sendMail();
			}
		}
		return $success;
	}
	
	static function phrasebook_to_array($xml_data, $lang)
	{
		$pb_array = array();
		
		if (!empty($xml_data))
		{
			$dom_doc = self::get_dom_doc($xml_data);
			$language_nodes = $dom_doc->getElementsByTagName('dictionary');
			
			if($language_nodes)
			{
				foreach($language_nodes as $lang_node)
				{
					if($lang_node->getAttribute('name') == $lang)
					{
						$phrase_nodes = $lang_node->getElementsByTagName('phrase');
						
						foreach($phrase_nodes as $phrase)
						{
							$key = $phrase->getAttribute('name');
							$value = $phrase->nodeValue;

							$pb_array[$key] = $value;
						}
					}
				}
			}
		}
		
		return $pb_array;
	}
		
	static function get_dom_doc($xml_source, $encoding='UTF-8')
	{
//		print("xml: $xml_source<br/>\n");//TMP
	
		$dom_document = new DOMDocument('1.0', $encoding);
		$success = $dom_document->loadXML($xml_source);
		
		if ($success)
		{
			return $dom_document;
		}
		else
		{
			return FALSE;
		}
	}
} // end NCE_form

/**
 *
 */
class TagObject
{
	var $params = array();
	var $name;
	var $html;
	var $inner_html;
	var $key; // for finding this tag content in the main tagdata
	var $is_single_tag = true;
	
	function __construct($tag_name, $params)
	{
		$this->name = $tag_name;		
		$this->init_params($params);
	}
	
	function replace_self($source_html)
	{
		return $this->replace_with($source_html, $this->inner_html);
	}
	
	function replace_with($source_html, $output_html)
	{
		$final = str_replace($this->html, $output_html, $source_html);
		return $final;
	}
	
	function replace_self_regex($source_html)
	{
		return $this->replace_with_regex($source_html, $this->inner_html);
	}
	
	function replace_with_regex($source_html, $output_html)
	{		
//		print("replacing:<pre>$output_html\n</pre><hr/>in:<pre>$source_html\n</pre>");//TMP
		
		$key = preg_quote($this->key);
		if ($this->is_single_tag)
		{
			$regex = "/".LD."$key"."(.*?)".RD."/s";
		}
		else
		{
			$regex = "/".LD."$key".RD."(.*?)".LD.SLASH."$this->name".RD."/s";				
		}	
		
//		print($regex."<br/>\n");//TMP
//		print("<hr/>\ninner html: $this->inner_html\n<hr/>----\n");//TMP
//		print("<hr/>\output html: $output_html\n<hr/>----\n");//TMP
		$final = preg_replace($regex, $output_html, $source_html);

		return $final;	
	}	
	
	function init_params($params)
	{
		if (!is_array($params)) // convert parameter string to array (from single tags)
		{
			$temp = array();
			preg_match_all("'([^ ]+)=\"([^\"]+)\"'siU", $params, $matches);
						
			for ($i = 0; $i < sizeof($matches[1]); $i++)
			{
				$key = $matches[1][$i];
				$temp[$key] = $matches[2][$i];
			}
			
			$params = $temp;
		}
	
		if (sizeof($params) > 0)
		{
			foreach ($params as $key => $val)
			{
				$val = html_entity_decode($val);
				$values = explode(",", $val);
				$vals = array();
				foreach ($values as $v)
				{
					$parts = explode(":", $v);
					$k = $parts[0];
					$vals[$k] = $parts[1];
				}
								
				$this->params[$key] = $vals; // multiple values
			}
			
			foreach ($this->params as $key => $val) // move things out of single-element arrays 
			{
				if (is_array($val) && sizeof($val) == 1)
				{
					foreach ($val as $k => $v)
					{
						if (empty($v))
						{
							unset($this->params[$key]);
							$this->params[$key] = $k;
						}
					}
				}
			}
		}
	} // end init_params
	
	function create_inner_tag_objects($name="")
	{
		//print_r($this->inner_html);//TMP
		$tag_objects = self::create_tag_objects($this->inner_html, $name);
		
		return $tag_objects;
	}
	
	function single_params_to_string()
	{
		$param_string = "";
	
		foreach ($this->params as $key => $value)
		{
			if (!is_array($value))
			{
				$param_string .= "$key=\"$value\" ";
			}
		}
		
		return trim($param_string);
	}
	
	static function create_tag_objects($html, $name="")
	{
		global $TMPL, $FNS;
		
		$tag_objects = array();
		
		foreach ($TMPL->var_single as $key => $val)
		{
			preg_match_all("/".LD."$key"."(.*?)".RD."/s", $html, $matches);
			$parts = split(" ", $key);
			$tag_name = $parts[0];
			//print_r($key);//TMP
			if (empty($name) || $tag_name == $name)
			{
				for ($i = 0; $i < sizeof($matches[0]); $i++)
				{
					$tag_html = $matches[0][$i];
					$tag_inner_html = $matches[1][$i];
					$tag_object = new TagObject($tag_name, $val);
					$tag_object->is_single_tag = true;
					$tag_object->key = $key;
					$tag_object->html = $tag_html;
					$tag_objects[] = $tag_object;
				}
			}			
		}			

		foreach ($TMPL->var_pair as $key => $val)
		{
			$parts = explode(" ", $key);
			$tag_name = $parts[0];
			$regex = LD."$key".RD."(.*?)".LD.SLASH."$tag_name".RD;
			preg_match_all("/".$regex."/s", $html, $matches);

			if (empty($name) || $tag_name == $name)
			{			
				for ($i = 0; $i < sizeof($matches[0]); $i++)
				{
					$tag_html = $matches[0][$i];
					$tag_inner_html = $matches[1][$i];
					$tag_object = new TagObject($tag_name, $val);
					$tag_object->is_single_tag = false;
					$tag_object->key = $key;
					$tag_object->html = $tag_html;
					$tag_object->inner_html = $tag_inner_html;
					$tag_objects[] = $tag_object;
				}
			}
		}	

		return $tag_objects;

	} // end create_tag_objects	

} // end TagObject

/**
 * Represents a form field, contains
 * - tag object for the field
 * - tag objects for the elements of a field
 * - validation code
 * - html output code for form elements
 */
class FieldObject
{
	var $field_tag;
	var $element_tags;
	var $error_tags;
	var $name;
	var $type;
	var $default_option;
	var $is_multi_element = false;
	
	function __construct($field_tag)
	{
		$this->init($field_tag);
//		print_r($this->field_tag->params['validation']);//TMP
	}
	
	function init($field_tag)
	{
		$this->field_tag = $field_tag;
		$this->name = $this->field_tag->params['name'];
		$this->type = $this->field_tag->params['type'];
		$this->default_option = $this->field_tag->params['default'];
//		print("default: $this->default_option<br/>\n");//TMP
		$this->element_tags = $this->field_tag->create_inner_tag_objects('element');
		$this->is_multi_element = (sizeof($this->element_tags) > 1);
		
		$this->error_tags = $this->field_tag->create_inner_tag_objects('error');
//		print("elements for $this->name ($this->type):\n");//TMP
//		print_r($this->element_tags);//TMP
	}
	
	/**
	 * remove the error tags from this field's html
	 */
	function clear_error_tags()
	{
//		print("Field: $this->name clear_error_tags()<hr/>\n");//TMP
	
		if (sizeof($this->error_tags) > 0)
		{
			foreach ($this->error_tags as $tag)
			{
//				print("replacing error tag for field $this->name type {$tag->params['validation']}<br/>\n");//TMP
				$this->field_tag->inner_html = str_replace($tag->html, "", $this->field_tag->inner_html);
//				print("field tag inner_html:<hr/>\n{$this->field_tag->inner_html}\n---------\n<hr/>");//TMP
			}
		}
	}
	
	// do validation and output any errors in the field html
	function validate()
	{
//		print("$this->name validate()<br/>\n");//TMP
	
		// output the validation errors
		$validation_types = $this->field_tag->params['validation'];
		if (!is_array($validation_types) && !empty($validation_types))
		{
			$validation_types = array($validation_types => '');
		}

//		print("validation types for $this->name
		if (sizeof($validation_types) > 0)
		{		
			$element_value = $this->get_submitted_value();
		
			$errors = array();
		
//			print("validation on type $this->type element_name: $element_name<br/>\n");//TMP
		
			foreach($validation_types as $type => $value)
			{
//				print("'$type' validation on type $this->type field: $this->name<br/>\n");//TMP
				$is_error = false;
			
//				print("<hr>\n");//TMP
			
				switch ($type)
				{
					case 'required' :
						if (!$this->is_multi_element || $this->type == 'radio')
						{
							$is_error = empty($element_value);
//							print("doing single element $type validation for $this->name on $element_name (value: $element_value) error? ($is_error)<br/>\n");//TMP
						}
						else
						{
							$is_error = true;
							foreach ($element_value as $v)
							{
								$is_error &= empty($v);
//								print("doing multi-element $type validation for $this->name on $element_name (value: $element_value) error? ($is_error)<br/>\n");//TMP
							}
						}
						break;
						
					case 'number' :
						if (!$this->is_multi_element)
						{
							$is_error = !self::is_valid_number($element_value);
//							print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error)<br/>\n");//TMP
						}
						break;
						
					case 'not_numbers' :
						if (!$this->is_multi_element)
						{
							$bad_chars = "0-9";
							$is_error = preg_match("/([".$bad_chars."])+/i", $element_value, $matches);
//							print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error) bad chars: {$matches[1]}<br/>\n");//TMP
						}
						break;
					
					case 'names' :
						if (!$this->is_multi_element)
						{
							$bad_chars = "�`!�\$%^&\*\(\)_\+=~#'@;:<>\/\\?\|0123456789";
							$is_error = preg_match("/([".$bad_chars."])+/i", $element_value, $matches);
//							print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error) bad chars: {$matches[1]}<br/>\n");//TMP
						}
						break;
						
					case 'letters' :
						if (!$this->is_multi_element)
						{
							$bad_chars = "�`!�\$%^&\*\(\)_\+=~#'@;:<>,.\/\\?\|0123456789-";
							$is_error = preg_match("/([".$bad_chars."])+/i", $element_value, $matches);
//							print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error) bad chars: {$matches[1]}<br/>\n");//TMP
						}
						break;
						
					case 'email' :
						if (!$this->is_multi_element)
						{
							$is_error = !self::is_valid_email($element_value);
//							print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error)<br/>\n");//TMP
						}
						break;
						
					case 'minlength' :
						$is_error = (strlen($element_value) < $value);
						//print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error)<br/>\n");//TMP
						break;

					case 'maxlength' :
						$is_error = (strlen($element_value) > $value);
						//print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error)<br/>\n");//TMP
						break;

					case 'age_greater_than' :
						$date = $element_value;
						$min_age = $value;
						$is_error = !self::is_valid_age($min_age, $date['years'], $date['months'], $date['days']);
						//print("doing $type validation for $this->name on $element_name (value: $element_value) error? ($is_error)<br/>\n");//TMP
						break;
						
					case 'captcha' :
						if (!$this->is_multi_element)
						{
							$is_error = !self::is_valid_captcha($element_value);
						}
						break;
				}
				
				if ($is_error)
				{
					$errors[$type] = true;
				}
			}	
			
//			print("errors for $this->name: ");//TMP
//			print_r($errors);//TMP
			
			foreach ($this->error_tags as $tag) // output validation errors
			{
				$v_type = $tag->params['validation'];
//				print("field '$this->name' error tag for validation type: $v_type<br/>\n");//TMP
				if (array_key_exists($v_type, $errors)) // output the error tag html within the field tag
				{
//					print("$type error found on $element_name<br/>\n");//TMP
					$this->field_tag->inner_html = str_replace($tag->html, $tag->inner_html, $this->field_tag->inner_html);
				}
				else // remove the error tag html from this field
				{
//					print("NO $type ERROR ON $element_name<br/>\n");//TMP
					$this->field_tag->inner_html = str_replace($tag->html, "", $this->field_tag->inner_html);
				}
			}
		}
		
		$is_valid = (sizeof($errors) == 0);
		
		return $is_valid;
	}
	
	/**
	 * @return A single value or array of values submitted in the form for this field 
	 *  i.e. multiple choice returns an array for each chosen option, date returns an array of date components. e.g. array('years' => 1989, 'months' => '10', days => '23')
	 */
	function get_submitted_value()
	{
		$value;
		
		// pick the name of the one-and-only element in the field (or special case for radio as all elements share the field name)
		if (!$this->is_multi_element || $this->type == 'radio') 
		{
			$tag = $this->element_tags[0];
			$element_name = $tag->params['name'];
			$element_name = (empty($element_name) ? $this->name : $element_name); // element has no explicit name, use the field name
			$value = trim($_REQUEST[$element_name]);
		}		
		else
		{
			foreach ($this->element_tags as $tag)
			{
				$element_name = $tag->params['name'];
				if($this->type == 'multiple_choice')
				{
					$element_name = 'ncf_group_'.$this->name.'_'.$element_name;					
				}
				$v = $_REQUEST[$element_name];
				$val = ($v == 'on' ? $element_name : $v); // for checkboxes
				if (!empty($val))
				{
					$val = (empty($tag->inner_html) ? $val : $tag->inner_html); // use the label for checkboxes as the value if it was defined e.g. use 'First answer' in {element name="answer_1"}First answer{/element}
				}
				$value[$element_name] = $val;
			}
			
			if ($this->type == 'date')
			{
				$date = array();
				foreach ($this->element_tags as $tag)
				{
					$option_tags = TagObject::create_tag_objects($tag->inner_html);
					$date_part = $option_tags[0]->name;
					
					$element_value = ($_REQUEST[$this->name.'_'.$date_part]);
					$date[$date_part] = $element_value;
				}
				$value = $date;
			}
		}

//		print("submitted value for field '$this->name'<br/>\n");//TMP
//		print_r($value);//TMP
//		print("<hr/>");//TMP
		
		return $value;
	}
	
	function output_self($source_html)
	{
//		print_r($_REQUEST);//TMP
	
		$output_html = $source_html;
				
		// output all the elements in the field
		foreach ($this->element_tags as $tag)
		{
			$element_inner_html = $tag->inner_html;
			$element_html = "";
		
			$element_name = $tag->params['name'];
			$element_name = (empty($element_name) ? $this->name : $element_name); // take the name of the field as the name if the element doesn't have one

			// values for elements come from form submission if there was one, otherwise default values are used (if they exist)
			$is_element_submitted = isset($_REQUEST[$element_name]);
			$submitted_value = $_REQUEST[$element_name]; // @todo - might need to clean this up e.g. strip_slashes, url_decode etc.
		
			switch ($this->type) // output the HTML markup for each form element
			{
				case 'textfield' :
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$value = (empty($submitted_value) ? $element_inner_html : $submitted_value);
					$element_html = "<input id=\"$this->name\" name=\"$this->name\" type=\"text\" value=\"$value\" $attributes />\n";
					break;
					
				case 'password' :
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$value = (empty($submitted_value) ? $element_inner_html : $submitted_value);
					$element_html = "<input id=\"$this->name\" name=\"$this->name\" type=\"password\" value=\"$value\" $attributes />\n";
					break;
					
				case 'textarea' :
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$value = (empty($submitted_value) ? $element_inner_html : $submitted_value);
					$element_html = "<textarea id=\"$this->name\" name=\"$this->name\" $attributes >$value</textarea>\n";
					break;
					
				case 'radio' :
//					print("radio $this->name {$tag->params['value']} submitted_value: $submitted_value default: $this->default_option<hr/>\n");//TMP
					if ($tag->params['value'] == $submitted_value || (!empty($this->default_option) && $tag->params['value'] == $this->default_option))
					{
						$tag->params['checked'] = 'checked';
					}
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$element_html = "<input id=\"$this->name\" name=\"$this->name\" type=\"radio\" $attributes /> $tag->inner_html\n";
					break;
					
				case 'checkbox' :
					if ($is_element_submitted)
					{
						$tag->params['checked'] = 'checked';
					}
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$element_html = "<input id=\"$this->name\" name=\"$this->name\" type=\"checkbox\" value=\"".$tag->params['value']."\" $attributes /> $tag->inner_html\n";
					break;					
					
				case 'multiple_choice' :
					$grouped_name = "ncf_group_".$this->name."_$element_name";
					unset($tag->params['name']);
					$is_element_submitted = isset($_REQUEST[$grouped_name]);
					
					if ($is_element_submitted || (!$is_element_submitted && !empty($this->default_option) && $tag->params['name'] == $this->default_option))
					{
						$tag->params['checked'] = 'checked';
					}
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$element_html = "<input id=\"$grouped_name\" name=\"$grouped_name\" type=\"checkbox\" $attributes /> $tag->inner_html\n";
					break;
					
				case 'dropdown' :
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$options_tags = $tag->create_inner_tag_objects('option');
					$element_html = "<select id=\"$this->name\" name=\"$this->name\" $attributes>\n";
					foreach ($options_tags as $option_tag)
					{
						if (($is_element_submitted && $option_tag->params['value'] == $submitted_value) || (!empty($this->default_option) && $option_tag->params['value'] == $this->default_option))
						{
							$option_tag->params['selected'] = 'selected';
						}
						$attributes = $option_tag->single_params_to_string(); // tag attributes e.g. class="whatever"
						$element_html .= "<option $attributes >".$option_tag->inner_html."</option>\n";
					}
					$element_html .= "\n</select>\n";
					break;

				case 'date' :
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$inner_tags = $tag->create_inner_tag_objects();
					$date_part = $inner_tags[0]->name; // day, month or year
					$now_year = date("Y", time()); 
					$now_month = date("m", time()); 
					$now_day = date("d", time()); 
					$start_year = $this->field_tag->params['start_year'];
					$start_year = (empty($start_year) ? $now_year - 100 : $start_year);
					$end_year = $this->field_tag->params['end_year'];
					$end_year = (empty($end_year) ? $now_year : $end_year);
					
					$grouped_name = $this->name."_$date_part";
					$selected_option = $_REQUEST[$grouped_name];
					
					$element_html = "<select id=\"$grouped_name\" name=\"$grouped_name\" $attributes>\n";					
					
					switch ($date_part)
					{
						case 'day':
						case 'days':
							$start = 1;
							$end = 31;
							$current = $now_day;
							break;
						case 'month':
						case 'months':
							$start = 1;
							$end = 12;
							$current = $now_month;
							break;
						case 'year':
						case 'years':
							$start = $end_year;
							$end = $start_year;
							$current = $now_year;
							break;
					}
					
//					print("now year: $now_year start: $start end: $end current: $current<br/>\n");//TMP
					
					$selected_option = (empty($selected_option) ? $current : $selected_option);

					if ($start <= $end)
					{
						for ($i = $start; $i <= $end; $i++)
						{
							$selected = ($selected_option == $i ? 'selected="selected"' : '');
							$option_label = (strlen($i) < 2 ? "0".$i : $i);
							$element_html .=  "<option value=\"$i\" $selected >$option_label</option>\n";
						}
					}
					else
					{
						for ($i = $start; $i >= $end; $i--)
						{
							$selected = ($selected_option == $i ? 'selected="selected"' : '');
							$option_label = (strlen($i) < 2 ? "0".$i : $i);
							$element_html .=  "<option value=\"$i\" $selected >$option_label</option>\n";
						}
					}
					
					$element_html .= "\n</select>\n";
					break;
					
				case 'submit' :
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$element_html = "<input id=\"$this->name\" name=\"$this->name\" type=\"submit\" $attributes value=\"$element_inner_html\" />\n";
					break;
					
				case 'image' :
					$attributes = $tag->single_params_to_string(); // tag attributes e.g. class="whatever"
					$element_html = "<input id=\"$this->name\" name=\"$this->name\" type=\"image\" $attributes />\n";
					break;
					
				case 'hidden' :
					$value = (empty($submitted_value) ? $element_inner_html : $submitted_value);
					$element_html = "<input id=\"$this->name\" name=\"$this->name\" type=\"hidden\" value=\"$value\" />\n";
					break;
					
				default : //@todo - implement this later
					$element_html = "<strong>Sorry, $this->type field type (for field $this->name) not yet supported</strong>";
					break;
			}
		
//			print(htmlspecialchars($element_html)."<hr/>");//TMP
		
			if (!empty($element_html)) // only replace if field type is handled (otherwise leave it in the form)
			{
				$this->field_tag->inner_html = str_replace($tag->html, $element_html, $this->field_tag->inner_html);
			}
		}
		
		$output_html = $this->field_tag->replace_self_regex($output_html);
		
		return $output_html;
	}
	
	static function is_valid_number($value)
	{
		return !preg_match("/[^0-9]+/siU", $value);
	}
	
	static function is_valid_email($value)
	{
		$bad_chars = ",`!\$%^&\*\(\)\+=~#';:<>\/\\?\| ";
		$is_valid = preg_match("/^([^".$bad_chars."])+@([^".$bad_chars."])+\.([^".$bad_chars."])+$/i", $value, $matches);
		return (bool)$is_valid;
//		return (bool)preg_match("/^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$/", $value);
	}
	
	static function is_valid_age($min_age, $year, $month, $day)
	{
		$valid = false;
		
		$now = time();
		$today = date("d.m.Y", $now);
		list($current_day, $current_month, $current_year) = explode("." , $today);
		
		$year_dif = $current_year - $year;
		if(($month > $current_month) || ($month == $month && $current_day < $day))
		{
			$year_dif--;
		}
		//print($year_dif);//TMP
		if($year_dif >= $min_age)
		{
			$valid = true;
		}
		
		return $valid;
	}
	
	function is_valid_captcha($value)
	{
		global $DB, $IN;
		
		$res = $DB->query("SELECT COUNT(*) AS count FROM exp_captcha WHERE word='".$DB->escape_str($value)."' AND ip_address = '".$IN->IP."' AND date > UNIX_TIMESTAMP()-7200");
	
		if ($res->row['count'] == 0) // captcha incorrect
		{
//			$error = $LANG->line('captcha_incorrect');//TMP
//			print("error: $error<br/>\n");//TMP
			return false;
		}
		else // captcha is ok, so remove it from the DB
		{
			$DB->query("DELETE FROM exp_captcha WHERE (word='".$DB->escape_str($value)."' AND ip_address = '".$IN->IP."') OR date < UNIX_TIMESTAMP()-7200");
		}
		
		return true;
	}
} // end FieldObject

?>